<?php
#
# dmBridge: a data access framework for CONTENTdm(R)
#
# Copyright © 2009, 2010, 2011 Board of Regents of the Nevada System of Higher
# Education, on behalf of the University of Nevada, Las Vegas
#

/**
 * Manages all configuration operations involving the config.xml file.
 *
 * @author Alex Dolski <alex.dolski@unlv.edu>
 * @license http://www.opensource.org/licenses/mit-license.php
 */
final class DMConfigXML {

	/**
	 * Array of Singleton instances, one per file
	 */
	private static $instances = array();

	/** string */
	private $config_xml_pathname;
	/** string */
	private $data_dir;
	/** DOMDocument */
	private $dxml;

	/**
	 * @return string
	 */
	private static function getDefaultXmlPathname() {
		return dirname(__FILE__) . "/../../data/config.xml";
	}

	/**
	 * @param string xml_file_pathname
	 * @return DMConfigXML
	 */
	public static function getInstance($xml_file_pathname = null) {
		if (!$xml_file_pathname) {
			$xml_file_pathname = self::getDefaultXmlPathname();
		}
		$xml_file_pathname = realpath($xml_file_pathname);
		if (!array_key_exists($xml_file_pathname, self::$instances)) {
			self::$instances[$xml_file_pathname] = new DMConfigXML(
					$xml_file_pathname);
		}
		return self::$instances[$xml_file_pathname];
	}

	/**
	 * @param DMConfigXML instance
	 * @return void
	 */
	public static function destroyInstance(DMConfigXML $instance) {
		foreach (self::$instances as $pathname => $inst) {
			if ($inst == $instance) {
				unset(self::$instances[$pathname]);
				break;
			}
		}
	}

	/**
	 * @param string xml_file_pathname
	 */
	protected function __construct($xml_file_pathname) {
		$this->config_xml_pathname = $xml_file_pathname;
	}

	public function __clone() {
		//trigger_error("Cannot clone a Singleton");
	}

	public function __wakeup() {
		//trigger_error("Cannot deserialize a Singleton");
	}

	/**
	 * @return Indexed array of dmTemplate objects, sorted by name
	 * @since 0.3
	 */
	public function getAllTemplateSets() {
		$sets = array();
		$xp = new DOMXPath($this->getDomDocument());
		foreach ($xp->query('templates/set/@id') as $node) {
			$sets[] = new DMTemplateSet($node->nodeValue);
		}
		sort($sets);
		return $sets;
	}

	/**
	 * Modifies $col by reference.
	 *
	 * @param DMCollection col
	 * @return void
	 */
	public function loadCollection(DMCollection $col) {
		// overview URI
		$uri = $this->getCollectionPropertyValue($col, "/overviewURI");
		if ($uri) {
			$uri = new DMURI($uri);
			$col->setOverviewURI($uri);
		}

		// image URI
		$uri = $this->getCollectionPropertyValue($col, "/image512URI");
		if ($uri) {
			$uri = new DMURI($uri);
			$col->setImage512URI($uri);
		}

		// description
		$desc = $this->getCollectionPropertyValue($col, '/description');
		$col->setDescription($desc);

		// grid view
		$result = $this->getCollectionPropertyValues($col, "gridView/field",
				false);
		$col->setHasCustomResultsViewPrefs($result);

		$result = $this->getCollectionPropertyValues($col, "gridView/field");
		foreach ($result as $node) {
			$f = $col->getField($node->getAttribute('nick'));
			if (!$f instanceof DMDCElement) {
				$f = new DMDCElement($node->getAttribute('nick'));
			}
			$col->addGridViewField($f);
		}

		// facets
		$result = $this->getCollectionPropertyValues($col,
				"/resultsView/facets/field", false);
		$col->setHasCustomResultsViewPrefs($result);

		$result = $this->getCollectionPropertyValues($col,
				"/resultsView/facets/field");
		foreach ($result as $node) {
			$field = new DMDCElement($node->getAttribute('nick'));
			foreach ($col->getFields() as $f) {
				if ($f->getNick() == $node->getAttribute('nick')) {
					$field->setName($f->getName());
				}
			}
			$col->addFacet(new DMFacetTerm($field));
		}

		// search view
		$result1 = $this->getCollectionPropertyValue(
				$col, "/searchView/dateSearch/beginYear", false);
		$result2 = $this->getCollectionPropertyValue(
				$col, "/searchView/dateSearch/endYear", false);
		$col->setHasCustomSearchViewPrefs($result1 || $result2);

		$col->setDateSearchRange(
			$this->getCollectionPropertyValue(
					$col, "/searchView/dateSearch/beginYear"),
			$this->getCollectionPropertyValue(
					$col, "/searchView/dateSearch/endYear"));

		// reference url redirection
		$col->setRedirectingReferenceURLs(
			(bool) $this
				->getCollectionPropertyValue($col, "/redirectReferenceURLs"));

		// field preferences
		$result = $this->getCollectionPropertyValues($col,
				"/resultsView/fields/field", false);
		$col->setHasCustomResultsViewPrefs($result);

		$result = $this->getCollectionPropertyValues($col,
				"/resultsView/fields/field");
		foreach ($result as $xf) {
			foreach ($col->getFields() as $cf) {
				$cf->setCollection($col);
				if ($xf->getAttribute('nick') == $cf->getNick()) {
					$cf->setSortable($xf->getAttribute('isSortable'));
					$cf->setDefaultSort($xf->getAttribute('isDefaultSort'));
					$cf->setMaxWords((int) $xf->getAttribute('maxWords'));
				}
			}
		}

		// template set
		switch ($this->getCollectionPropertyValue($col,
				"/templateSets/templateSet/@use", false)) {
		case "id":
			$col->setTemplateSetID(
				$this->getCollectionPropertyValue($col,
						"/templateSets/templateSet/@id"));
			break;
		default:
			$col->setUsingDefaultTemplateSet(true);
			break;
		}

		// viewer definitions
		$result = $this->getCollectionPropertyValues($col, "/objectView/media",
				false);
		$col->setHasCustomObjectViewPrefs($result);
		
		$result = $this->getCollectionPropertyValues($col, "/objectView/media");
		foreach ($result as $node) {
			$vd = new DMObjectViewerDefinition(
					DMMediaType::getTypeForString($node->getAttribute("type")));
			$vd->setClass(
				$node->getElementsByTagName("class")->item(0)->nodeValue);
			$filesize = $node->getElementsByTagName("maxFileSizeK");
			$vd->setMaxFileSize(
				$filesize->length ? $filesize->item(0)->nodeValue * 1000 : null);
			$vd->setWidth(
				$node->getElementsByTagName("maxWidth")->item(0)->nodeValue);
			$vd->setHeight(
				$node->getElementsByTagName("maxHeight")->item(0)->nodeValue);
			$col->addViewerDefinition($vd);
		}
	}

	/**
	 * @param DMCollection col
	 * @throws DMIOException
	 * @return void
	 */
	public function saveCollection(DMCollection $col) {
		// delete old collection node if it exists
		$xpath = sprintf("collections/collection[@alias = '%s']",
			$col->getAlias());
		$this->removeNode($xpath);

		// get collections node
		$parent_node = $this->getNode("collections");
		$col_node = $this->getDomDocument()->createElement("collection");
		$col_node->setAttribute("alias", $col->getAlias());
		$parent_node->appendChild($col_node);

		// overview URI
		if ($col->getAlias() != "/dmdefault") {
			$node = $this->getDomDocument()->createElement("overviewURI",
				DMString::xmlentities($col->getOverviewURI()));
			$col_node->appendChild($node);
		}

		// image URI
		if ($col->getAlias() != "/dmdefault") {
			$node = $this->getDomDocument()->createElement("image512URI",
				DMString::xmlentities($col->getImage512URI()));
			$col_node->appendChild($node);
		}

		// description
		if ($col->getAlias() != "/dmdefault") {
			$node = $this->getDomDocument()->createElement("description",
				DMString::xmlentities($col->getDescription()));
			$col_node->appendChild($node);
		}

		// redirect reference URLs
		if ($col->getAlias() != "/dmdefault") {
			$node = $this->getDomDocument()->createElement("redirectReferenceURLs",
				(int) $col->isRedirectingReferenceURLs());
			$col_node->appendChild($node);
		}

		// search view
		if ($col->getDateSearchBeginYear() || $col->getDateSearchEndYear()) {
			$sv_node = $this->getDomDocument()->createElement("searchView");
			$ds_node = $this->getDomDocument()->createElement("dateSearch");
			$sv_node->appendChild($ds_node);
			$col_node->appendChild($sv_node);

			// date search begin year
			$year_node = $this->getDomDocument()->createElement("beginYear",
				$col->getDateSearchBeginYear());
			$ds_node->appendChild($year_node);

			// date search end year
			$year_node = $this->getDomDocument()->createElement("endYear",
				$col->getDateSearchEndYear());
			$ds_node->appendChild($year_node);
		}

		// grid view
		if (count($col->getGridViewFields())) {
			$gv_node = $this->getDomDocument()->createElement("gridView");
			$col_node->appendChild($gv_node);
			foreach ($col->getGridViewFields() as $f) {
				if ($f instanceof DMDCElement) {
					if (strlen($f->getNick()) < 1) {
						continue;
					}
					$fnode = $this->getDomDocument()->createElement("field");
					$fnode->setAttribute("nick", $f->getNick());
					$gv_node->appendChild($fnode);
				}
			}
		}

		// template sets
		$ts_node = $this->getDomDocument()->createElement("templateSets");
		$col_node->appendChild($ts_node);

		$d_node = $this->getDomDocument()->createElement("templateSet");
		$ts_node->appendChild($d_node);
		if ($col->getTemplateSet() instanceof DMTemplateSet
				&& $col->getTemplateSet()->getID()) {
			$d_node->setAttribute("use", "id");
			$d_node->setAttribute("id", $col->getTemplateSet()->getID());
		} else {
			$d_node->setAttribute("use", "default");
		}

		// results view
		$rv_node = $this->getDomDocument()->createElement("resultsView");
		$col_node->appendChild($rv_node);

		// fields
		$fi_node = $this->getDomDocument()->createElement("fields");
		$rv_node->appendChild($fi_node);
		$added_fields = 0;
		foreach ($col->getFields() as $f) {
			if (strlen($f->getNick()) < 1) {
				continue;
			}
			if ($f->getNick() == "index" || $f->getNick() == "thumb") {
				continue;
			}
			// only save fields that are sortable, default sort, and/or have max
			// words
			if (!$f->isSortable() && !$f->isDefaultSort() && !$f->getMaxWords()) {
				continue;
			}
			$fnode = $this->getDomDocument()->createElement("field");
			$fnode->setAttribute("nick", $f->getNick());
			$fnode->setAttribute("isSortable", (int) $f->isSortable());
			$fnode->setAttribute("isDefaultSort", (int) $f->isDefaultSort());
			$fnode->setAttribute("maxWords", (int) $f->getMaxWords());
			$fi_node->appendChild($fnode);
			$added_fields++;
		}
		if ($added_fields) {
			$rv_node->appendChild($fi_node);
		}

		// facets
		$fa_node = $this->getDomDocument()->createElement("facets");
		$rv_node->appendChild($fa_node);
		$added_fields = 0;
		foreach ($col->getFacets() as $f) {
			$fnode = $this->getDomDocument()->createElement("field");
			$fnode->setAttribute("nick", $f->getField()->getNick());
			$fa_node->appendChild($fnode);
			$added_fields++;
		}
		if ($added_fields) {
			$rv_node->appendChild($fa_node);
		}

		// viewer definitions
		$added_fields = 0;
		$ov_node = $this->getDomDocument()->createElement("objectView");
		foreach ($col->getViewerDefinitions() as $d) {
			$media_node = $this->getDomDocument()->createElement("media");
			$media_node->setAttribute("type", $d->getMediaType());
			$class_node = $this->getDomDocument()->createElement("class",
				$d->getClass());
			$max_size_node = $this->getDomDocument()->createElement("maxFileSizeK",
				$d->getMaxFileSize() / 1000);
			$width_node = $this->getDomDocument()->createElement("maxWidth",
				$d->getWidth());
			$height_node = $this->getDomDocument()->createElement("maxHeight",
				$d->getHeight());
			$media_node->appendChild($class_node);
			$media_node->appendChild($max_size_node);
			$media_node->appendChild($width_node);
			$media_node->appendChild($height_node);
			$ov_node->appendChild($media_node);
			$added_fields++;
		}
		if ($added_fields) {
			$col_node->appendChild($ov_node);
		}

		$this->save();
	}

	/**
	 * @return bool
	 * @since 2.0
	 */
	public function isCommentingEnabled() {
		return (bool) $this->getNodeValue("comments/@enabled");
	}

	/**
	 * @param Boolean bool
	 * @since 2.0
	 */
	public function setCommentingEnabled($bool) {
		$this->setNodeValue('comments/@enabled', (int) $bool);
	}

	/**
	 * @return bool
	 * @since 0.1
	 */
	public function isCommentModerationEnabled() {
		return (bool) $this->getNodeValue("comments/moderation/@enabled");
	}

	/**
	 * @param Boolean bool
	 */
	public function setCommentModerationEnabled($bool) {
		$this->setNodeValue('comments/moderation/@enabled', (int) $bool);
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getCommentNotificationEmail() {
		return $this->getNodeValue("comments/email");
	}

	/**
	 * @param string str Email to which comment notifications should be sent
	 */
	public function setCommentNotificationEmail($str) {
		$this->setNodeValue('comments/email', $str);
	}

	/**
	 * @return bool
	 * @since 0.1
	 */
	public function isCommentNotificationEnabled() {
		return (bool) $this->getNodeValue("comments/email/@enabled");
	}

	/**
	 * @param Boolean bool
	 */
	public function setCommentNotificationEnabled($bool) {
		$this->setNodeValue("comments/email/@enabled", (int) $bool);
	}

	/**
	 * @return string Absolute path of the data directory, with no trailing
	 * slash.
	 * @since 0.2
	 */
	public function getDataDir() {
		$root = DMConfigIni::getInstance()->getString("dmbridge.data_path");
		if (!$this->data_dir) {
			if ($root) {
				$this->data_dir = str_replace("\\", "/", $root);
			} else {
				$this->data_dir = realpath(dirname(__FILE__) . "/../../data");
			}
		}
		return $this->data_dir;
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getFeedCopyright() {
		return $this->getNodeValue("feed/copyright");
	}

	/**
	 * @param string str
	 */
	public function setFeedCopyright($str) {
		$this->setNodeValue('feed/copyright', $str);
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getFeedLanguage() {
		return $this->getNodeValue("feed/language");
	}

	/**
	 * @param string str
	 */
	public function setFeedLanguage($str) {
		$this->setNodeValue('feed/language', $str);
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getFeedManagingEditorEmail() {
		return $this->getNodeValue("feed/managingEditor/email");
	}

	/**
	 * @param string str
	 */
	public function setFeedManagingEditorEmail($str) {
		$this->setNodeValue('feed/managingEditor/email', $str);
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getFeedManagingEditorName() {
		return $this->getNodeValue("feed/managingEditor/name");
	}

	/**
	 * @param string str
	 */
	public function setFeedManagingEditorName($str) {
		$this->setNodeValue('feed/managingEditor/name', $str);
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getFeedTitle() {
		return $this->getNodeValue("feed/title");
	}

	/**
	 * @param string str
	 */
	public function setFeedTitle($str) {
		$this->setNodeValue("feed/title", $str);
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getFeedSubtitle() {
		return $this->getNodeValue("feed/subtitle");
	}

	/**
	 * @param string str
	 */
	public function setFeedSubtitle($str) {
		$this->setNodeValue("feed/subtitle", $str);
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getFeedWebMasterEmail() {
		return $this->getNodeValue("feed/webMaster/email");
	}

	/**
	 * @param string str
	 */
	public function setFeedWebMasterEmail($str) {
		$this->setNodeValue('feed/webMaster/email', $str);
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getFeedWebMasterName() {
		return $this->getNodeValue("feed/webMaster/name");
	}

	/**
	 * @param string str
	 */
	public function setFeedWebMasterName($str) {
		$this->setNodeValue('feed/webMaster/name', $str);
	}

	/**
	 * @return string
	 */
	public function getFullPath() {
		return $this->config_xml_pathname;
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getInstitutionName() {
		return $this->getNodeValue("institution/name");
	}

	/**
	 * @param string str
	 * @return void
	 */
	public function setInstitutionName($str) {
		$this->setNodeValue("institution/name", $str);
	}

	/**
	 * @return bool
	 * @since 0.1
	 */
	public function isLoggingEnabled() {
		return (bool) $this->getNodeValue("logging/@enabled");
	}

	private function getNextAvailableTemplateSetID() {
		$id = 1;
		$xp = new DOMXPath($this->getDomDocument());
		foreach ($xp->query('//templates/set/@id') as $c) {
			$id = ((int) $c->nodeValue > $id) ? (int) $c->nodeValue : $id;
		}
		return $id + 1;
	}

	/**
	 * @param mixed module A DMBridgeModule object, or the name of a module.
	 * @return bool
	 * @since 2.0
	 */
	public function isModuleEnabled($module) {
		if ($module instanceof DMBridgeModule) {
			$module = $module->getName();
		}
		$xpath = sprintf("//modules/module[name = '%s']/@enabled", $module);
		return (bool) $this->getNodeValue($xpath);
	}

	/**
	 * Used to check whether a module is installed with the same name as the
	 * given module. A module is installed the first time it is activated, and
	 * remains installed forever, even if deactivated.
	 * 
	 * @param DMBridgeModule module
	 * @return boolean
	 * @see isModuleOfSameNameAndVersionInstalled
	 */
	public function isModuleOfSameNameInstalled(DMBridgeModule $module) {
		$xpath = sprintf("//modules/module[name = '%s']",
				$module->getName());
		return ($this->getNodeList($xpath)->length);
	}

	/**
	 * Used to check whether a module is installed with the same name and
	 * version as the given module. A module is installed the first time it is
	 * activated, and remains installed forever, even if deactivated.
	 * 
	 * @param DMBridgeModule module
	 * @return boolean
	 * @see isModuleOfSameNameInstalled
	 */
	public function isModuleOfSameNameAndVersionInstalled(
			DMBridgeModule $module) {
		$xpath = sprintf("//modules/module[name = '%s' and version = '%s']",
				$module->getName(), $module->getVersion());
		return ($this->getNodeList($xpath)->length);
	}

	/**
	 * @param DMBridgeModule module
	 * @param bool enabled
	 * @return void
	 */
	public function setModuleEnabled(DMBridgeModule $module, $enabled) {
		// delete old module node if it exists
		$xpath = sprintf("modules/module[name = '%s' and version = '%s']",
			$module->getName(), $module->getVersion());
		$this->removeNode($xpath);

		// create new module node
		$module_node = $this->getDomDocument()->createElement("module");
		$module_node->setAttribute("enabled", (int) $enabled);
		$name_node = $this->getDomDocument()->createElement("name",
				$module->getName());
		$version_node = $this->getDomDocument()->createElement("version",
				$module->getVersion());

		$module_node->appendChild($name_node);
		$module_node->appendChild($version_node);
		$this->getDomDocument()->getElementsByTagName("modules")->item(0)
				->appendChild($module_node);
	}

	/**
	 * @return bool
	 * @since 2.0
	 */
	public function isRatingEnabled() {
		return (bool) $this->getNodeValue("rating/@enabled");
	}

	/**
	 * @param Boolean bool
	 * @since 2.0
	 */
	public function setRatingEnabled($bool) {
		$this->setNodeValue("rating/@enabled", (int) $bool);
	}

	/**
	 * @return bool
	 * @since 2.0
	 */
	public function isTaggingEnabled() {
		return (bool) $this->getNodeValue("tagging/@enabled");
	}

	/**
	 * @param Boolean bool
	 * @since 2.0
	 */
	public function setTaggingEnabled($bool) {
		$this->setNodeValue("tagging/@enabled", (int) $bool);
	}

	/**
	 * @return bool
	 * @since 0.3
	 */
	public function isTagModerationEnabled() {
		return (bool) $this->getNodeValue("tagging/@moderated");
	}

	/**
	 * @param Boolean bool
	 */
	public function setTagModerationEnabled($bool) {
		$this->setNodeValue("tagging/@moderated", (int) $bool);
	}

	/**
	 * @param DMTemplateSet set
	 * @return Boolean
	 * @throws DMConstraintException
	 */
	public function deleteTemplateSet(DMTemplateSet $set) {
		// first check to make sure no collections are using it.
		$normal_xpath = "//collection/templateSets/templateSet/@id";
		$dxp = new DOMXPath($this->getDomDocument());

		$result = $dxp->query($normal_xpath);
		$in_use = false;
		foreach ($result as $node) {
			if ($node->value == $set->getID()) {
				$in_use = true;
			}
		}

		if ($in_use) {
			throw new DMConstraintException(
					DMLocalizedString::getString("TPL_IN_USE"));
		}

		// delete template node
		if ($set->getID()) {
			$xpath = sprintf("templates/set[@id = '%d']", $set->getID());
			$this->removeNode($xpath);
		}
		$this->save();
	}

	/**
	 * Modifies $set by reference.
	 *
	 * @param DMTemplateSet set
	 * @return void
	 * @throws DMUnavailableModelException
	 * @since 0.3
	 */
	public function loadTemplateSet(DMTemplateSet $set) {
		$xpath = sprintf("//templates/set[@id = '%d']", $set->getID());
		$result = $this->getNode($xpath);
		if (!$result instanceof DOMNode) {
			throw new DMUnavailableModelException(
				DMLocalizedString::getString("INVALID_TPL_SET"));
		}
		$set->setName($result->getAttribute("name"));
		$alias = null;
		if ($result->getElementsByTagName("set")->item(0) instanceof DOMNode) {
			$alias = $result->getElementsByTagName("set")->item(0)
					->getAttribute("default");
		}

		$xpath = sprintf("//templates/set[@id = '%d']/collections/@default",
			$set->getID());
		$alias = $this->getNodeValue($xpath);
		try {
			$col = DMCollectionFactory::getCollection($alias);
		} catch (DMUnavailableModelException $e) {
			$collections = DMCollection::getAuthorized();
			$col = $collections[0];
		}
		$set->setDefaultCollection($col);

		// results view fields
		$result = $this->getTemplateSetPropertyValues(
				"/resultsView/gridView/field", $set);
		foreach ($result as $node) {
			$set->addGridViewField(
					new DMDCElement($node->getAttribute('nick')));
		}

		// tile view columns
		$result = $this->getTemplateSetPropertyValue(
				"/resultsView/tileView/columns", $set);
		$set->setNumTileViewColumns((int) $result);

		// results per page
		$result = $this->getTemplateSetPropertyValue(
				"/resultsView/resultsPerPage", $set);
		$set->setNumResultsPerPage($result);

		// authorized collections
		$xpath = sprintf(
			"//templates/set[@id = '%d']/collections/allowed/collection/@alias",
			DMString::xmlentities($set->getID()));
		$dxp = new DOMXPath($this->getDomDocument());
		foreach ($dxp->query($xpath) as $alias) {
			$set->addAuthorizedCollection(
					DMCollectionFactory::getCollection($alias->value));
		}

	}

	/**
	 * @param DMTemplateSet set
	 * @return Boolean
	 * @throws DMException If name or path are not unique
	 * @throws DMIOException
	 */
	public function saveTemplateSet(DMTemplateSet $set) {
		// delete old template node if it exists
		if ($set->getID()) {
			$xpath = sprintf("templates/set[@id = '%d']", $set->getID());
			$this->removeNode($xpath);
		} else { // find next id
			$set->setID($this->getNextAvailableTemplateSetID());
		}

		// make sure name is unique
		$xpath = sprintf("templates/set[@id != '%d' and @name = '%s']",
			$set->getID(),
			DMString::xmlentities($set->getName())
		);
		$dxp = new DOMXPath($this->getDomDocument());
		if ($dxp->query($xpath)->length) {
			throw new DMException(
					DMLocalizedString::getString("TPL_NAME_EXISTS"));
		}

		// get templates node
		$tpl_node = $this->getNode("templates");
		$set_node = $this->getDomDocument()->createElement("set");
		$set_node->setAttribute("id", $set->getID());
		$set_node->setAttribute("name", $set->getName());
		$tpl_node->appendChild($set_node);

		// default collection
		$col_node = $this->getDomDocument()->createElement("collections");
		$col_node->setAttribute(
			'default', $set->getDefaultCollection()->getAlias()
		);
		$set_node->appendChild($col_node);

		// authorized collections
		if (count($set->getAuthorizedCollections())) {
			$allowed_node = $this->getDomDocument()->createElement("allowed");
			$col_node->appendChild($allowed_node);
			foreach ($set->getAuthorizedCollections() as $c) {
				$col = $this->getDomDocument()->createElement("collection");
				$col->setAttribute("alias", $c->getAlias());
				$allowed_node->appendChild($col);
			}
		}

		$rv_node = $this->getDomDocument()->createElement("resultsView");
		$set_node->appendChild($rv_node);

		// results per page
		if ($set->getNumResultsPerPage()) {
			$rpp_node = $this->getDomDocument()->createElement("resultsPerPage",
					$set->getNumResultsPerPage());
			$rv_node->appendChild($rpp_node);
		}

		// grid view fields
		if (count($set->getGridViewFields())) {
			$gv_node = $this->getDomDocument()->createElement("gridView");
			foreach ($set->getGridViewFields() as $f) {
				$fnode = $this->getDomDocument()->createElement("field");
				$fnode->setAttribute("nick", $f->getNick());
				$gv_node->appendChild($fnode);
			}
			$rv_node->appendChild($gv_node);
		}

		// tile columns
		if ($set->getNumTileViewColumns()) {
			$tv_node = $this->getDomDocument()->createElement("tileView");
			$col_node = $this->getDomDocument()->createElement(
				"columns", $set->getNumTileViewColumns());
			$tv_node->appendChild($col_node);
			$rv_node->appendChild($tv_node);
		}

		//$this->getDomDocument()->formatOutput = true;
		//die($this->getDomDocument()->saveXML());

		$this->save();
	}

	/**
	 * @param DMCollection col
	 * @return DMTemplateSet, or null
	 * @since 0.4
	 */
	public function getTemplateSetForCollection(DMCollection $col) {
		$use = $this->getCollectionPropertyValue(
				"/templateSets/templateSet/@use", $col);
		switch ($use) {
			case "id":
				$id = $this->getCollectionPropertyValue(
					"/templateSets/templateSet/@id", $col);
				break;
			default:
				$default_col = DMCollectionFactory::getCollection("/dmdefault");
				$id = $this->getCollectionPropertyValue(
					"/templateSets/templateSet/@id", $default_col);
				break;
		}
		return new DMTemplateSet($id);
	}

	/**
	 * @return Boolean
	 */
	public function isValid() {
		libxml_use_internal_errors(true);
		@$this->getDomDocument()->schemaValidate(
			dirname(__FILE__) . "/../../includes/config.xsd");
	}

	/**
	 * Pulls the version from VERSION.txt, which is created automatically by
	 * the packaging tool.
	 *
	 * @return string
	 * @since 0.1
	 */
	public function getVersion() {
		$tmp = $this->parseVersionFile();
		return $tmp[0];
	}

	/**
	 * Will increment by 1 with each subsequent release.
	 *
	 * @return int
	 * @since 0.1
	 */
	public function getVersionSequence() {
		$tmp = $this->parseVersionFile();
		return $tmp[1];
	}

	/**
	 * @param string rel_path Path to the data file's subdirectory, relative to
	 * the dmbridgedata directory
	 * @throws DMIOException
	 * @return DOMDocument
	 */
	protected function getDomDocument() {
		if (!$this->dxml instanceof DOMDocument) {
			// if a file doesn't exist at $abs_path, attempt to create it
			$did_create = false;
			if (!file_exists($this->config_xml_pathname)) {
				$source_path = dirname(__FILE__)
					. "/../../data/config.default.xml";
				$result = copy($source_path, $this->config_xml_pathname);
				if (!$result) {
					throw new DMIOException(sprintf(
							DMLocalizedString::getString("ERROR_WRITING_FILE"),
								$this->config_xml_pathname));
				}
				$did_create = true;
			}
			$this->dxml = new DOMDocument("1.0", "utf-8");
			$this->dxml->preserveWhiteSpace = false;
			if (!$this->dxml->load($this->config_xml_pathname)) {
				throw new DMIOException(sprintf(
						DMLocalizedString::getString("ERROR_WRITING_FILE"),
							$this->config_xml_pathname));
			}
			if ($did_create) {
				// config.default.xml contains references to the "/oclctest"
				// collection, which may not be available; so change these to
				// a collection that is available
				$all_collections = DMCollection::getAuthorized();
				if (count($all_collections)) {
					$this->setNodeValue("//collections/@default",
							$all_collections[0]->getAlias());
				}
				// assign the basic template set (ID 1) to each collection
				foreach (DMCollection::getAuthorized() as $col) {
					$col->setUsingDefaultTemplateSet(true);
					$this->saveCollection($col);
				}
			}
		}
		return $this->dxml;
	}

	/**
	 * @param DMCollection col
	 * @param string rel_xpath XPath statement relative to
	 * "collections/collection[@alias]/"
	 * @param bool load_default
	 * @see getCollectionPropertyValues()
	 */
	private function getCollectionPropertyValue(DMCollection $col, $rel_xpath,
			$load_default = true) {
		$str = "collections/collection[@alias='%s']/" . ltrim($rel_xpath, "/");
		$xpath = sprintf($str, $col->getAlias());
		$value = $this->getNodeValue($xpath);
		if (strlen($value) > 0) {
			return $value;
		}
		if ($load_default) {
			$xpath = sprintf($str, "/dmdefault");
			return $this->getNodeValue($xpath);
		}
		return null;
	}

	/**
	 * @param DMCollection col
	 * @param string rel_xpath XPath statement relative to
	 * "collections/collection[@alias]/"
	 * @param bool load_default
	 * @see getCollectionPropertyValue()
	 */
	private function getCollectionPropertyValues(DMCollection $col, $rel_xpath,
			$load_default = true) {
		$str = "collections/collection[@alias='%s']/" . ltrim($rel_xpath, "/");
		$xpath = sprintf($str, $col->getAlias());
		$result = $this->getNodeList($xpath);
		if ($result->length > 0) {
			return $result;
		}
		if ($load_default) {
			$xpath = sprintf($str, "/dmdefault");
			return $this->getNodeList($xpath);
		}
		return null;
	}

	private function getNode($xpath) {
		$tmp = $this->getNodeList($xpath);
		return $tmp->item(0);
	}

	private function getNodeList($xpath) {
		$xp = new DOMXPath($this->getDomDocument());
		return $xp->query($xpath);
	}

	private function getNodeValue($xpath) {
		$node = $this->getNode($xpath);
		if ($node instanceof DOMElement) {
			return $node->nodeValue;
		}
		if ($node instanceof DOMNode) {
			return $node->value;
		}
		return null;
	}

	private function setNodeValue($xpath, $value) {
		$node = $this->getNode($xpath);
		if ($value || $value === 0) {
			if ($node instanceof DOMElement) {
				$node->nodeValue = $value;
				$node->removeAttribute("xsi:nil");
			} else if ($node instanceof DOMAttr) {
				$node->value = $value;
			}
		} else if (!$value) {
			if ($node instanceof DOMElement) {
				$node->nodeValue = "";
				$node->setAttribute("xsi:nil", "true");
			} else if ($node instanceof DOMAttr) {
				$node->parentNode->removeAttributeNode($node);
			}
		}
	}

	private function removeNode($xpath) {
		$xp = new DOMXPath($this->getDomDocument());
		$result = $xp->query($xpath);
		if ($result->item(0)) {
			if ($result->item(0)->parentNode instanceof DOMNode) {
				$result->item(0)->parentNode->removeChild($result->item(0));
			}
		}
	}

	/**
	 * @param string rel_path Path to the XML file's directory, relative to the
	 * dmbridgedata directory
	 * @throws DMIOException
	 */
	public function save() {
		// append generator attributes to root element
		$now = new DMDateTime();
		$this->dxml->documentElement->setAttribute("generator", "dmBridge");
		$this->dxml->documentElement->setAttribute("generatorVersion",
			DMBridgeVersion::getDmBridgeVersion());
		$this->dxml->documentElement->setAttribute("generated",
			$now->asISO8601());

		$this->dxml->formatOutput = true;

		if (@$this->getDomDocument($this->config_xml_pathname)->save(
				$this->config_xml_pathname)) {
		} else {
			throw new DMIOException(
				sprintf(DMLocalizedString::getString("ERROR_WRITING_FILE"),
						$this->config_xml_pathname));
		}
	}

	/**
	 * @param string rel_xpath XPath statement relative to "//templates/set[@id]"
	 * @param DMTemplateSet ts
	 */
	private function getTemplateSetPropertyValue($rel_xpath, DMTemplateSet $ts) {
		$str = "//templates/set[@id = '%d']/" . ltrim($rel_xpath, '/');
		$xpath = sprintf($str, $ts->getID());
		return $this->getNodeValue($xpath);
	}

	/**
	 * @param string rel_xpath XPath statement relative to "//templates/set[@id]"
	 * @param DMTemplateSet ts
	 */
	private function getTemplateSetPropertyValues($rel_xpath, DMTemplateSet $ts) {
		$str = "//templates/set[@id = '%d']/" . ltrim($rel_xpath, '/');
		$xpath = sprintf($str, $ts->getID());
		return $this->getNodeList($xpath);
	}

	/**
	 * @return array with version at key 0 and version sequence at key 1
	 */
	private function parseVersionFile() {
		$file = dirname(__FILE__) . '/../../VERSION.txt';
		if (file_exists($file)) {
			$data = file_get_contents($file);
			$tmp = explode("\n", $data);
			$version = (string) substr($tmp[0], 0, 20);
			$seq = (int) substr($tmp[1], 0, 3);
			return array($version, $seq);
		}
		return array(null, null);
	}

}
